# detection/detection_widget.py
import socket
import json
import os
import yaml
import traceback
from PyQt5.QtWidgets import (
    QWidget, QVBoxLayout, QMenuBar, QMenu, QAction,
    QFrame, QGridLayout, QLabel, QSplitter, QTextEdit, QLineEdit, QPushButton,
    QComboBox, QSlider, QCheckBox, QFileDialog, QInputDialog, QMessageBox
)
from PyQt5.QtCore import Qt, QSettings  # 导入 QSettings 用于存储最近文件
from PyQt5.QtGui import QPixmap
import glob

# 参数对话框
from detection.train_params_dialog import TrainParamsDialog
from detection.inference_params_dialog import InferenceParamsDialog
from detection.model_params_dialog import ModelParamsDialog
# 鼠标缩放支持
from widgets.zoomable_label import ZoomableLabel

# 可选导入YOLO，如果安装了的话
try:
    from ultralytics import YOLO
    YOLO_AVAILABLE = True
except ImportError:
    YOLO_AVAILABLE = False
    print("Warning: ultralytics not installed. Batch processing will be limited.")


class DetectionWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.train_params = {}
        self.infer_params = {}
        self.infer_thread = None
        self.train_thread = None
        # 缓存最后一帧检测图用于保存
        self.last_detected_pixmap = None
        self._last_input_source = None
        # 用于显示检测统计信息
        self.detection_stats_label = None

        self.train_thread = None
        # 新增：缓存最后一帧检测图用于保存
        self.last_detected_pixmap = None
        self._last_input_source = None
        # 新增：用于显示检测统计信息
        self.detection_stats_label = None
        # --- 新增：定义模型目录 ---
        # 假设 DetectionWidget.py 位于 yolo_app/detection/ 目录下
        # self.models_dir 将指向 yolo_app/models/
        self.models_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models")
        # 确保 models 目录存在
        os.makedirs(self.models_dir, exist_ok=True)

        # 缓存批量处理模型
        self._batch_model = None
        self._batch_model_path = None

        self._setup_ui()
        self._setup_menus()
        self._load_train_yaml()
        self._load_infer_yaml()
        # 同步UI控件与配置文件参数
        self._sync_ui_with_configs()
        # 注意：最近文件菜单在 _setup_menus 中创建和初始化
        # 网络服务
        self.SERVER_HOST = "127.0.0.1"
        self.SERVER_PORT = 5001 # 与服务器监听的端口一致

    # ------------------- UI -------------------
    def _setup_ui(self):
        self.layout = QVBoxLayout(self)

        # 左侧控制区
        self.left_frame = QFrame()
        self.left_layout = QVBoxLayout(self.left_frame)

        # 文件/输入源
        file_frame = QFrame()
        file_layout = QGridLayout(file_frame)
        file_layout.addWidget(QLabel("输入源:"), 0, 0)
        self.input_path_edit = QLineEdit()
        file_layout.addWidget(self.input_path_edit, 0, 1, 1, 2)
        self.btn_open_img = QPushButton("打开图片")
        self.btn_open_video = QPushButton("打开视频")
        self.btn_camera = QPushButton("启动摄像头")
        file_layout.addWidget(self.btn_open_img, 1, 0)
        file_layout.addWidget(self.btn_open_video, 1, 1)
        file_layout.addWidget(self.btn_camera, 1, 2)
        self.left_layout.addWidget(file_frame)

        # 模型配置区
        model_frame = QFrame()
        model_layout = QGridLayout(model_frame)

        # YOLO版本选择 (保留)
        model_layout.addWidget(QLabel("YOLO版本:"), 0, 0)
        self.combo_model_version = QComboBox()
        self.combo_model_version.addItems(["YOLOv5", "YOLOv8", "YOLOv9", "YOLO11"])
        model_layout.addWidget(self.combo_model_version, 0, 1)

        # --- 修改：模型文件选择 (替代原来的 combo_weights) ---
        # 模型文件路径
        model_layout.addWidget(QLabel("模型文件:"), 1, 0)
        self.model_path_edit = QLineEdit()
        self.model_path_edit.setReadOnly(True) # 设为只读，通过按钮选择
        model_layout.addWidget(self.model_path_edit, 1, 1, 1, 2) # 跨两列

        # 选择预训练模型按钮
        self.btn_browse_pretrained = QPushButton("选择预训练模型")
        # 加载自定义模型按钮
        self.btn_browse_custom = QPushButton("加载自定义模型")
        model_layout.addWidget(self.btn_browse_pretrained, 2, 0)
        model_layout.addWidget(self.btn_browse_custom, 2, 1)
        # --- 修改结束 ---

        # Confidence slider (推理用)
        model_layout.addWidget(QLabel("置信度"), 3, 0) # 行号调整
        self.slider_conf = QSlider(Qt.Horizontal)
        self.slider_conf.setMinimum(10)
        self.slider_conf.setMaximum(100)
        self.slider_conf.setValue(25)
        model_layout.addWidget(self.slider_conf, 3, 1) # 行号调整

        # IOU slider (推理用)
        model_layout.addWidget(QLabel("IOU"), 4, 0) # 行号调整
        self.slider_iou = QSlider(Qt.Horizontal)
        self.slider_iou.setMinimum(10)
        self.slider_iou.setMaximum(100)
        self.slider_iou.setValue(45)
        model_layout.addWidget(self.slider_iou, 4, 1) # 行号调整

        self.left_layout.addWidget(model_frame)

        # 操作控制区
        control_frame = QFrame()
        control_layout = QGridLayout(control_frame)
        self.btn_start = QPushButton("开始检测")
        self.btn_pause = QPushButton("暂停/继续")
        self.btn_stop = QPushButton("停止检测")
        self.btn_save = QPushButton("保存结果")
        control_layout.addWidget(self.btn_start, 0, 0)
        control_layout.addWidget(self.btn_pause, 0, 1)
        control_layout.addWidget(self.btn_stop, 1, 0)
        control_layout.addWidget(self.btn_save, 1, 1)
        self.chk_show_box = QCheckBox("显示目标框")
        self.chk_show_label = QCheckBox("显示类别标签")
        self.chk_save_log = QCheckBox("保存日志")
        control_layout.addWidget(self.chk_show_box, 2, 0)
        control_layout.addWidget(self.chk_show_label, 2, 1)
        control_layout.addWidget(self.chk_save_log, 3, 0)
        self.left_layout.addWidget(control_frame)

        # 状态区
        status_frame = QFrame()
        status_layout = QGridLayout(status_frame)
        self.status_label = QLabel("状态: 就绪")
        self.fps_label = QLabel("FPS: 0.0")
        self.error_label = QLabel("错误提示: 无")
        self.btn_help = QPushButton("帮助")
        self.btn_about = QPushButton("关于")
        self.btn_exit = QPushButton("退出")
        status_layout.addWidget(self.status_label, 0, 0)
        status_layout.addWidget(self.fps_label, 0, 1)
        status_layout.addWidget(self.error_label, 1, 0, 1, 2)
        status_layout.addWidget(self.btn_help, 2, 0)
        status_layout.addWidget(self.btn_about, 2, 1)
        status_layout.addWidget(self.btn_exit, 3, 0, 1, 2)
        self.left_layout.addWidget(status_frame)

        # 右侧显示区
        self.right_splitter = QSplitter(Qt.Vertical)
        self.label_orig = ZoomableLabel("原始画面")
        self.label_orig.setAlignment(Qt.AlignCenter)
        self.label_det = ZoomableLabel("检测画面")
        self.label_det.setAlignment(Qt.AlignCenter)

        # 创建用于显示检测统计信息的 QLabel
        self.detection_stats_label = QLabel("检测统计: 就绪")
        self.detection_stats_label.setWordWrap(True)
        self.detection_stats_label.setStyleSheet("background-color: #f0f0f0; padding: 5px; border: 1px solid #ccc;")
        self.detection_stats_label.setMinimumHeight(30)

        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        top_splitter = QSplitter(Qt.Horizontal)
        top_splitter.addWidget(self.label_orig)
        top_splitter.addWidget(self.label_det)
        self.right_splitter.addWidget(top_splitter)
        # 添加统计标签到分割器
        self.right_splitter.addWidget(self.detection_stats_label)
        self.right_splitter.addWidget(self.log_text)

        # 主布局
        main_splitter = QSplitter(Qt.Horizontal)
        main_splitter.addWidget(self.left_frame)
        main_splitter.addWidget(self.right_splitter)
        self.layout.addWidget(main_splitter)

        # ------------------- 信号绑定 -------------------
        self.btn_open_img.clicked.connect(self.open_image)
        self.btn_open_video.clicked.connect(self.open_video)
        self.btn_camera.clicked.connect(self.open_camera)
        # 按钮连接到推理菜单动作 (在 _setup_menus 后)
        # self.btn_start.clicked.connect(lambda: self.infer_start_action.trigger())
        # self.btn_pause.clicked.connect(lambda: self.infer_pause_action.trigger())
        # self.btn_stop.clicked.connect(lambda: self.infer_stop_action.trigger())

        self.btn_save.clicked.connect(self.save_result)
        self.btn_help.clicked.connect(lambda: self.log_text.append("帮助: 请参阅使用说明"))
        self.btn_about.clicked.connect(lambda: self.log_text.append("关于: YOLO检测软件 v1.0"))
        self.btn_exit.clicked.connect(self.close_app)

        # --- 修改：连接新的模型按钮信号 ---
        self.btn_browse_pretrained.clicked.connect(self._browse_pretrained_model)
        self.btn_browse_custom.clicked.connect(self._browse_custom_model)
        # --- 修改结束 ---

        # 连接滑块和复选框信号到更新函数
        self.slider_conf.valueChanged.connect(self._on_conf_slider_changed)
        self.slider_iou.valueChanged.connect(self._on_iou_slider_changed)
        self.chk_show_box.stateChanged.connect(self._on_show_box_changed)
        self.chk_show_label.stateChanged.connect(self._on_show_label_changed)
        self.chk_save_log.stateChanged.connect(lambda s: self.log_text.append(f"保存检测日志: {'是' if s else '否'}"))

    # ------------------- 菜单 -------------------
    def _setup_menus(self):
        self.menubar = QMenuBar(self)

        # --- 文件菜单 ---
        (self.file_menu,
         self.open_img_action,
         self.open_video_action,
         self.open_cam_action,
         self.save_result_action,
         self.batch_process_action,
         self.exit_app_action) = self._create_file_menu()

        self.file_actions = [
            self.open_img_action, self.open_video_action, self.open_cam_action,
            self.save_result_action, self.batch_process_action, self.exit_app_action
        ]
        self.menubar.addMenu(self.file_menu)

        # --- 模型菜单 ---

        (self.model_menu,
        self.load_model_action,
        self.model_library_action,
        self.model_params_action) = self._create_model_menu() # 返回值调整
        self.menubar.addMenu(self.model_menu)

        # --- 新增：创建独立的数据菜单 ---
        self.data_menu, self.load_data_config_action = self._create_data_menu()
        self.menubar.addMenu(self.data_menu)

        # --- 训练菜单 ---
        self.train_menu, self.train_param_action, self.train_start_action, self.train_pause_action, self.train_stop_action = self._create_train_menu()
        self.menubar.addMenu(self.train_menu)

        # --- 推理菜单 ---
        self.infer_menu, self.infer_param_action, self.infer_start_action, self.infer_pause_action, self.infer_stop_action = self._create_infer_menu()
        self.menubar.addMenu(self.infer_menu)

        # --- 关于菜单 ---
        self.about_menu = QMenu("关于", self)
        self.menubar.addMenu(self.about_menu)

        self.layout.setMenuBar(self.menubar)

        # --- 文件菜单动作连接 ---
        self.open_img_action.triggered.connect(self.open_image)
        self.open_video_action.triggered.connect(self.open_video)
        self.open_cam_action.triggered.connect(self.open_camera)
        self.save_result_action.triggered.connect(self.save_result)
        self.batch_process_action.triggered.connect(self.batch_process)
        self.exit_app_action.triggered.connect(self.close)

        # --- 模型菜单动作连接 ---
        # (在 _create_model_menu 中已连接部分)
        self.load_data_config_action.triggered.connect(self._load_data_config) # 新增

        # --- 训练菜单动作连接 ---
        # self.train_param_action.triggered.connect(self._open_train_params) # (在 _create_train_menu 中已连接)
        # self.train_start_action.triggered.connect(self._start_training) # (在 _create_train_menu 中已连接)

        # --- 推理菜单动作连接 ---
        # self.infer_param_action.triggered.connect(self._open_infer_params) # (在 _create_infer_menu 中已连接)
        # self.infer_start_action.triggered.connect(self._start_inference) # (在 _create_infer_menu 中已连接)
        # self.infer_pause_action.triggered.connect(self._pause_inference) # (在 _create_infer_menu 中已连接)
        # self.infer_stop_action.triggered.connect(self._stop_inference) # (在 _create_infer_menu 中已连接)

        # --- 界面按钮连接到推理菜单动作 ---
        if hasattr(self, 'btn_start') and hasattr(self, 'infer_start_action'):
            self.btn_start.clicked.connect(self.infer_start_action.trigger)
        if hasattr(self, 'btn_pause') and hasattr(self, 'infer_pause_action'):
            self.btn_pause.clicked.connect(self.infer_pause_action.trigger)
        if hasattr(self, 'btn_stop') and hasattr(self, 'infer_stop_action'):
            self.btn_stop.clicked.connect(self.infer_stop_action.trigger)

        # --- 新增：创建并插入最近文件子菜单 ---
        self.recent_files_menu = QMenu("最近文件", self)
        # 插入到 "保存检测结果" (self.file_actions[3]) 之前
        self.file_menu.insertMenu(self.file_actions[3], self.recent_files_menu)
        self._update_recent_files_menu() # 初始化内容

    # -------- 文件菜单 --------
    def _create_file_menu(self):
        menu = QMenu("文件", self)
        open_img = QAction("打开图片", self); open_img.setShortcut("Ctrl+O")
        open_video = QAction("打开视频", self); open_video.setShortcut("Ctrl+V")
        open_cam = QAction("打开摄像头", self); open_cam.setShortcut("Ctrl+C")
        save_result = QAction("保存检测结果", self); save_result.setShortcut("Ctrl+S")
        batch_process = QAction("批量处理", self)
        exit_app = QAction("退出", self); exit_app.setShortcut("Ctrl+Q")
        menu.addActions([open_img, open_video, open_cam, save_result, batch_process, exit_app])
        return menu, open_img, open_video, open_cam, save_result, batch_process, exit_app

    # -------- 模型菜单 --------
    def _create_model_menu(self):
        """创建模型菜单及其动作项 (不含数据集配置)"""
        menu = QMenu("模型", self)
        load_model = QAction("加载模型", self)
        model_library = QAction("模型库", self)
        # load_data_config = QAction("加载数据集配置", self) # <-- 移除
        model_params = QAction("模型参数", self)
        load_model.triggered.connect(self._load_model_file)
        model_library.triggered.connect(self._open_model_library)
        # load_data_config.triggered.connect(self._load_data_config) # <-- 移除
        model_params.triggered.connect(self._open_model_params)
        # menu.addActions([load_model, model_library, load_data_config, model_params]) # <-- 修改
        menu.addActions([load_model, model_library, model_params]) # <-- 修改：不添加 load_data_config
        # return menu, load_model, model_library, load_data_config, model_params # <-- 修改
        return menu, load_model, model_library, model_params # <-- 修改：返回值不包含 load_data_config

    def _load_model_file(self):
        """加载模型 (与 UI 按钮 '加载自定义模型' 功能一致)"""
        # file, _ = QFileDialog.getOpenFileName(self, "加载模型权重", "", "YOLO Models (*.pt *.onnx)")
        # --- 修改：限制为 .pt 文件，并设置初始目录为 models_dir ---
        file, _ = QFileDialog.getOpenFileName(self, "加载模型权重", self.models_dir, "PyTorch Models (*.pt)")
        # --- 修改结束 ---
        if file:
            self.log_text.append(f"[加载模型] 文件: {file}")
            # abs_path = os.path.abspath(file) # 可以使用绝对路径，但相对路径或文件名也可以
            abs_path = file # 直接使用用户选择的路径
            # --- 新增：更新 UI 和 infer.yaml，与 _browse_custom_model 保持一致 ---
            self.model_path_edit.setText(abs_path)
            self._update_infer_yaml("model", abs_path)

    def _open_model_library(self):
        """模型库 (从 yolo_app/models/ 目录扫描)"""
        try:
            os.makedirs(self.models_dir, exist_ok=True)
            pt_files = glob.glob(os.path.join(self.models_dir, "*.pt"))
            model_names = [os.path.basename(f) for f in pt_files]

            # 可选：添加标准模型名（如果目录中没有，用户选择后会触发下载）
            # standard_models = ["yolov8n.pt", "yolov8s.pt", "yolov8m.pt", "yolov8l.pt", "yolov8x.pt"]
            # for model in standard_models:
            #     if model not in model_names:
            #         model_names.append(model)

            if not model_names:
                self.log_text.append(f"[模型库] 目录 '{self.models_dir}' 中未找到 .pt 文件。")
                QMessageBox.information(self, "无模型", f"目录 '{self.models_dir}' 中没有找到 .pt 文件。")
                return

            model_names.sort()
            model, ok = QInputDialog.getItem(self, "模型库", "选择模型:", model_names, 0, False)
            if ok and model:
                # 构造完整路径 (优先使用 models_dir 下的文件)
                full_path = os.path.join(self.models_dir, model)
                self.log_text.append(f"[模型库] 选择模型: {full_path}")
                # 更新 UI
                self.model_path_edit.setText(full_path)
                # 更新 infer.yaml
                self._update_infer_yaml("model", full_path) # 使用完整路径

        except Exception as e:
            self.log_text.append(f"[错误] 模型库操作出错: {e}")
            import traceback
            self.log_text.append(traceback.format_exc())
# --- 修改结束 ---


    def _open_model_params(self):
        dlg = ModelParamsDialog(self, getattr(self, "model_params", None))
        if dlg.exec_():
            self.model_params = dlg.get_params()
            self.log_text.append(f"更新模型参数: {self.model_params}")

    def _create_data_menu(self):
        """创建数据菜单及其动作项"""
        menu = QMenu("数据", self)
        load_data_config = QAction("加载数据集配置", self)
        # 可以在这里添加其他与数据相关的操作，例如：
        # edit_data_config = QAction("编辑数据集配置", self)
        # generate_data_config = QAction("生成数据集配置模板", self)
        # menu.addActions([load_data_config, edit_data_config, generate_data_config])
        menu.addAction(load_data_config)
        return menu, load_data_config

    # --- 新增：加载数据集配置文件 ---
    def _load_data_config(self):
        """加载数据集配置文件 (data.yaml) 并同步到 train.yaml"""
        # file, _ = QFileDialog.getOpenFileName(self, "选择数据集配置文件", "", "YAML Files (*.yaml *.yml)")
        # --- 修改：设置初始目录 (例如项目根目录或一个 data 目录) ---
        initial_dir = os.path.dirname(os.path.dirname(__file__)) # yolo_app/
        # 或者 os.path.join(initial_dir, "datasets") 如果你有这个目录
        file, _ = QFileDialog.getOpenFileName(self, "选择数据集配置文件", initial_dir, "YAML Files (*.yaml *.yml)")
        # --- 修改结束 ---
        if file:
            self.log_text.append(f"[数据] 加载配置: {file}")
            abs_path = os.path.abspath(file)
            # 更新 train.yaml 中的 data 字段
            self._update_train_yaml("data", abs_path)
    # --- 新增结束 ---

    # --- 新增：浏览预训练模型 ---
    def _browse_pretrained_model(self):
        """浏览并选择预训练模型 (从 yolo_app/models/ 目录扫描)"""
        try:
            os.makedirs(self.models_dir, exist_ok=True)
            pt_files = glob.glob(os.path.join(self.models_dir, "*.pt"))
            model_names = [os.path.basename(f) for f in pt_files]

            # 可选：添加标准模型名
            # standard_models = ["yolov8n.pt", "yolov8s.pt", "yolov8m.pt", "yolov8l.pt", "yolov8x.pt"]
            # for model in standard_models:
            #     if model not in model_names:
            #         model_names.append(model)

            if not model_names:
                self.log_text.append(f"[预训练模型] 目录 '{self.models_dir}' 中未找到 .pt 文件。")
                QMessageBox.information(self, "无预训练模型", f"目录 '{self.models_dir}' 中没有找到 .pt 文件。")
                return

            model_names.sort()
            model, ok = QInputDialog.getItem(self, "选择预训练模型", "模型:", model_names, 0, False)
            if ok and model:
                full_path = os.path.join(self.models_dir, model)
                self.log_text.append(f"[预训练模型] 选择: {full_path}")
                # 更新 UI
                self.model_path_edit.setText(full_path)
                # 更新 infer.yaml
                self._update_infer_yaml("model", full_path)

        except Exception as e:
            self.log_text.append(f"[错误] 浏览预训练模型出错: {e}")
            import traceback
            self.log_text.append(traceback.format_exc())
    # --- 修改结束 ---s

    # --- 新增：浏览自定义模型 ---
    def _browse_custom_model(self):
        """浏览并选择自定义模型文件"""
        # file, _ = QFileDialog.getOpenFileName(self, "选择自定义模型权重", "", "PyTorch Models (*.pt)")
        # --- 修改：设置初始目录为 models_dir ---
        file, _ = QFileDialog.getOpenFileName(self, "选择自定义模型权重", self.models_dir, "PyTorch Models (*.pt)")
        # --- 修改结束 ---
        if file:
            self.log_text.append(f"[自定义模型] 加载: {file}")
            abs_path = os.path.abspath(file)
            # --- 确保：更新 UI 和 infer.yaml ---
            self.model_path_edit.setText(abs_path)
            self._update_infer_yaml("model", abs_path)
    # --- 新增结束 ---


    # -------- 训练菜单 --------
    def _create_train_menu(self):
        menu = QMenu("训练", self)
        param = QAction("调整参数", self)
        start = QAction("开始训练", self)
        pause = QAction("暂停训练", self)
        stop = QAction("停止训练", self)
        menu.addActions([param, start, pause, stop])
        param.triggered.connect(self._open_train_params)
        start.triggered.connect(self._start_training)
        # pause 和 stop 的逻辑可以根据 TrainThread 的实现来添加
        return menu, param, start, pause, stop

    def _start_training(self):
        from detection.Threads.train_threads import TrainThread
        self.train_thread = TrainThread(yaml_path="train.yaml")
        self.train_thread.log_signal.connect(lambda msg: self.log_text.append(msg))
        self.train_thread.finished.connect(lambda: self.log_text.append("[完成] 训练线程已结束"))
        self.train_thread.start()

    # -------- 推理菜单 --------
    def _create_infer_menu(self):
        menu = QMenu("推理", self)
        param = QAction("调整参数", self)
        start = QAction("开始推理", self); start.setShortcut("F5")
        pause = QAction("暂停推理", self); pause.setShortcut("F6")
        stop = QAction("停止推理", self); stop.setShortcut("F7")
        menu.addActions([param, start, pause, stop])
        param.triggered.connect(self._open_infer_params)
        start.triggered.connect(self._start_inference)
        pause.triggered.connect(self._pause_inference)
        stop.triggered.connect(self._stop_inference)
        return menu, param, start, pause, stop

    # 显示推理帧的方法
    def update_frames(self, orig_pixmap: QPixmap, det_pixmap: QPixmap):
        """接收线程推送的 QPixmap 并显示"""
        self.label_orig.setPixmap(orig_pixmap)
        self.label_det.setPixmap(det_pixmap)
        # 缓存最后一帧检测图用于保存
        self.last_detected_pixmap = det_pixmap

    # 新增：更新检测统计显示的方法
# 在 DetectionWidget.py 文件中找到 update_detection_stats 方法并替换为以下内容

    # --- 修改：update_detection_stats 方法，处理详细信息并发送到服务器 ---
    def update_detection_stats(self, detailed_stats_dict):
        """更新检测统计显示，并通过网络发送详细信息"""
        # --- 修改：更新 UI 显示 ---
        # 从 detailed_stats_dict 中提取类别计数用于显示
        counts = detailed_stats_dict.get('counts', {})
        if not counts:
            self.detection_stats_label.setText("检测统计: 无目标")
        else:
            stats_text = "检测统计: "
            # 按类别名排序
            items = [f"{cls_name}: {count}" for cls_name, count in sorted(counts.items())]
            stats_text += ", ".join(items)
            self.detection_stats_label.setText(stats_text)
        # --- 修改结束 ---

        # --- 新增/修改：通过 Socket 发送结果到服务器 ---
        # try:
        #     # 1. 从 detailed_stats_dict 中提取 detections 列表
        #     detections = detailed_stats_dict.get('detections', [])

        #     if detections:
        #         # 2. 格式化 xywh 为字符串，例如 "x1,y1,w1,h1;x2,y2,w2,h2;..."
        #         #    为了满足 { "class": "...", "xywh": "..." } 的要求，
        #         #    我们将所有 xywh 坐标拼接成一个分号分隔的字符串
        #         xywh_str_list = []
        #         for det in detections:
        #             # 将每个检测框的 xywh 坐标列表转换为逗号分隔的字符串
        #             xywh_coords = det['xywh']
        #             if isinstance(xywh_coords, (list, tuple)) and len(xywh_coords) == 4:
        #                 # 保留两位小数
        #                 coords_str = ",".join([f"{coord:.2f}" for coord in xywh_coords])
        #                 xywh_str_list.append(coords_str)
        #             else:
        #                 # 如果格式不对，添加占位符
        #                 xywh_str_list.append("0.00,0.00,0.00,0.00")

        #         xywh_str = ";".join(xywh_str_list)

        #         # 3. 格式化 class 为字符串，例如 "person,car,person"
        #         #    同样，为了满足要求，将所有类别名拼接成逗号分隔的字符串
        #         class_str_list = [det['class'] for det in detections]
        #         class_str = ",".join(class_str_list)

        #         # 4. 构造要发送的 JSON 数据
        #         data_to_send = {
        #             "class": class_str,
        #             "xywh": xywh_str
        #         }
        #     else:
        #         # 如果没有检测到目标，发送空字符串
        #         data_to_send = {
        #             "class": "",
        #             "xywh": ""
        #         }

        #     # 5. 创建 socket 客户端并连接到服务器
        #     client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        #     # 设置连接超时，避免长时间阻塞
        #     client_socket.settimeout(5.0)
        #     # 连接到服务器 (确保地址和端口与服务器一致)
        #     client_socket.connect((self.SERVER_HOST, self.SERVER_PORT))
        #     self.log_text.append(f"[网络] 已连接到服务器 {self.SERVER_HOST}:{self.SERVER_PORT}")

        #     # 6. 将数据序列化为 JSON 字符串并编码为字节
        #     json_data_str = json.dumps(data_to_send, ensure_ascii=False)
        #     client_socket.send(json_data_str.encode('utf-8'))
        #     self.log_text.append(f"[网络] 已发送数据: {json_data_str}")

        #     # 7. 接收服务器的回复
        #     try:
        #         client_socket.settimeout(5.0) # 设置接收超时
        #         response_bytes = client_socket.recv(1024)
        #         response_str = response_bytes.decode('utf-8')
        #         self.log_text.append(f"[网络] 服务器回复: {response_str}")

        #         # (可选) 解析服务器回复
        #         try:
        #             response_data = json.loads(response_str)
        #             if response_data.get("status") == "success":
        #                 self.log_text.append("[网络] 服务器确认接收成功。")
        #             else:
        #                 self.log_text.append(f"[网络] 服务器返回错误: {response_data.get('message', 'Unknown error')}")
        #         except json.JSONDecodeError:
        #             self.log_text.append(f"[网络] 服务器回复非JSON格式: {response_str}")

        #     except socket.timeout:
        #         self.log_text.append("[网络] 等待服务器回复超时。")
        #     except Exception as e:
        #         self.log_text.append(f"[网络] 接收服务器回复时出错: {e}")

        #     # 8. 关闭连接
        #     client_socket.close()
        #     self.log_text.append("[网络] 连接已关闭。")

        # except socket.timeout:
        #     error_msg = f"[网络错误] 连接到服务器 {self.SERVER_HOST}:{self.SERVER_PORT} 超时。"
        #     print(error_msg)
        #     self.log_text.append(error_msg)
        # except ConnectionRefusedError:
        #     error_msg = f"[网络错误] 无法连接到服务器 {self.SERVER_HOST}:{self.SERVER_PORT}。请确保服务器正在运行。"
        #     print(error_msg)
        #     self.log_text.append(error_msg)
        # except Exception as e: # 捕获连接、发送等所有可能的异常
        #     error_msg = f"[网络错误] 发送数据失败: {e}"
        #     print(error_msg)
        #     import traceback
        #     print(traceback.format_exc())
        #     self.log_text.append(error_msg)
        # --- 新增/修改结束 ---
    # --- 修改结束 ---

    # 开始推理
    def _start_inference(self):
        """开始推理"""
        input_path = self.input_path_edit.text().strip()
        if not input_path:
            self.log_text.append("[错误] 请输入图片/视频路径或选择摄像头ID")
            return

        if input_path.isdigit():
            input_source = int(input_path)
        else:
            input_source = input_path

        self._last_input_source = input_path

        from detection.Threads.infer_threads import InferThread
        self.infer_thread = InferThread(yaml_path="infer.yaml")
        self.infer_thread.input_source = input_source

        self.infer_thread.log_signal.connect(self._handle_infer_log)
        self.infer_thread.frame_signal.connect(self.update_frames)
        self.infer_thread.stats_signal.connect(self.update_detection_stats)
        self.infer_thread.finished.connect(lambda: self.log_text.append("[完成] 推理线程结束"))

        self.infer_thread.start()
        self.status_label.setText("状态: 运行中")

    # 新增一个方法来处理来自 InferThread 的日志
    def _handle_infer_log(self, message):
        """处理来自 InferThread 的日志消息"""
        # self.log_text.append(message)

    # 暂停推理
    def _pause_inference(self):
        if hasattr(self, "infer_thread") and self.infer_thread is not None and self.infer_thread.isRunning():
            if hasattr(self.infer_thread, "pause"):
                self.infer_thread.pause()
                status = "暂停" if self.infer_thread._is_paused else "继续"
                self.log_text.append(f"[推理] {status}")
                self.status_label.setText(f"状态: {status}")
            else:
                self.log_text.append("[警告] 推理线程未实现 pause 方法")
        else:
            self.log_text.append("[提示] 无正在运行的推理线程")

    # 停止推理
    def _stop_inference(self):
        if hasattr(self, "infer_thread") and self.infer_thread is not None and self.infer_thread.isRunning():
            self.infer_thread.stop()
            self.log_text.append("[推理] 已停止")
            self.status_label.setText("状态: 已停止")
        else:
            self.log_text.append("[提示] 无正在运行的推理线程")
            self.status_label.setText("状态: 就绪")

    # -------- 加载 train.yaml --------
    def _load_train_yaml(self):
        try:
            with open("train.yaml", "r", encoding="utf-8") as f:
                self.train_params = yaml.safe_load(f) or {}
        except FileNotFoundError:
            self.log_text.append("警告: train.yaml 文件未找到，使用默认配置。")
            self.train_params = {}
        except Exception as e:
            self.log_text.append(f"train.yaml 加载失败: {e}")
            self.train_params = {}

    # -------- 加载 infer.yaml --------
    def _load_infer_yaml(self):
        try:
            with open("infer.yaml", "r", encoding="utf-8") as f:
                self.infer_params = yaml.safe_load(f) or {}
        except FileNotFoundError:
             self.log_text.append("警告: infer.yaml 文件未找到，使用默认配置。")
             self.infer_params = self._get_default_infer_params()
        except Exception as e:
            self.log_text.append(f"infer.yaml 加载失败: {e}")
            self.infer_params = self._get_default_infer_params()

    def _get_default_infer_params(self):
        """获取默认推理参数"""
        return {
            "model": "yolov8n.pt",
            "conf": 0.25,
            "iou": 0.45,
            "show_box": True,
            "show_label": True,
            "show_conf": True
        }

    # -------- 安全的 YAML 更新函数 --------
    def _update_infer_yaml(self, key, value):
        """更新 infer.yaml 文件中的指定键值"""
        try:
            yaml_path = "infer.yaml"
            # 读取现有配置
            try:
                with open(yaml_path, "r", encoding="utf-8") as f:
                    config = yaml.safe_load(f) or {}
            except FileNotFoundError:
                config = {}

            # 更新配置
            config[key] = value

            # 写入配置
            with open(yaml_path, "w", encoding="utf-8") as f:
                yaml.dump(config, f, allow_unicode=True, default_flow_style=False)

            # 更新内存中的参数
            if not isinstance(self.infer_params, dict):
                self.infer_params = {}
            self.infer_params[key] = value

            # 如果推理线程正在运行，通知它参数已更新
            if (hasattr(self, 'infer_thread') and
                self.infer_thread is not None and
                self.infer_thread.isRunning()):
                self.infer_thread.update_params({key: value})

            self.log_text.append(f"[更新] infer.yaml: {key} = {value}")

        except Exception as e:
            error_msg = f"[错误] 更新 infer.yaml 失败: {e}\n{traceback.format_exc()}"
            self.log_text.append(error_msg)

    def _update_train_yaml(self, key, value):
        """更新 train.yaml 文件中的指定键值，保留其他内容和注释"""
        try:
            yaml_path = "train.yaml"
            if not os.path.exists(yaml_path):
                self.log_text.append(f"[警告] 训练配置文件不存在: {yaml_path}")
                return

            updated = False
            lines = []
            with open(yaml_path, "r", encoding="utf-8") as f:
                original_lines = f.readlines()

            for line in original_lines:
                stripped_line = line.strip()
                if not stripped_line or stripped_line.startswith("#"):
                    lines.append(line)
                    continue

                if ":" in stripped_line and not stripped_line.startswith(" "):
                    k = stripped_line.split(":", 1)[0].strip()
                    if k == key:
                        indent = line[:line.find(k)]
                        comment = ""
                        if "#" in line:
                            comment = line[line.find("#"):]
                        if isinstance(value, str):
                            new_line = f"{indent}{key}: '{value}' {comment}".rstrip() + "\n"
                        else:
                            new_line = f"{indent}{key}: {value} {comment}".rstrip() + "\n"
                        lines.append(new_line)
                        updated = True
                    else:
                        lines.append(line)
                else:
                    lines.append(line)

            if not updated:
                if lines and not lines[-1].endswith('\n'):
                    lines[-1] += '\n'
                if isinstance(value, str):
                    lines.append(f"{key}: '{value}'\n")
                else:
                    lines.append(f"{key}: {value}\n")

            with open(yaml_path, "w", encoding="utf-8") as f:
                f.writelines(lines)

            if not isinstance(self.train_params, dict):
                self.train_params = {}
            self.train_params[key] = value

            self.log_text.append(f"[更新] train.yaml: {key} = {value}")

        except Exception as e:
            error_msg = f"[错误] 更新 train.yaml 失败: {e}\n{traceback.format_exc()}"
            self.log_text.append(error_msg)

    # -------- 训练参数窗口 --------
    def _open_train_params(self):
        # 隐藏主窗口
        main_window = self.window()
        main_window.hide()

        dlg = TrainParamsDialog(yaml_path="train.yaml", parent=None)
        dlg.setWindowModality(Qt.ApplicationModal)
        dlg.exec_()

        # 显示主窗口
        main_window.show()

        # 获取并处理新参数
        new_values = dlg.get_params()
        if not new_values:
            return

        allowed_keys = list(dlg.param_desc.keys()) if hasattr(dlg, 'param_desc') else list(new_values.keys())

        try:
            with open("train.yaml", "r", encoding="utf-8") as f:
                lines = f.readlines()
        except Exception as e:
            self.log_text.append(f"[错误] 读取 train.yaml 失败: {e}")
            return

        for i, line in enumerate(lines):
            stripped = line.strip()
            if not stripped or stripped.startswith("#") or ":" not in stripped:
                continue
            key = stripped.split(":", 1)[0].strip()
            if key in allowed_keys and key in new_values:
                indent = line[:line.find(key)]
                comment = ""
                if "#" in line:
                    comment = line[line.find("#"):]
                lines[i] = f"{indent}{key}: {new_values[key]} {comment}".rstrip() + "\n"

        try:
            with open("train.yaml", "w", encoding="utf-8") as f:
                f.writelines(lines)
            self.log_text.append(f"[更新] 训练参数已保存到 train.yaml")
            # 重新加载以同步内存
            self._load_train_yaml()
        except Exception as e:
            self.log_text.append(f"[错误] 写入 train.yaml 失败: {e}")

    # -------- 推理参数窗口 --------
    def _open_infer_params(self):
        main_window = self.window()
        main_window.hide()
        dlg = InferenceParamsDialog(parent=None)
        if dlg.exec_():
            self.infer_params = dlg.get_params()
            self.log_text.append(f"推理参数更新: {self.infer_params}")
            try:
                with open("infer.yaml", "w", encoding="utf-8") as f:
                    yaml.dump(self.infer_params, f, allow_unicode=True, default_flow_style=False)
                self.log_text.append("[更新] 推理参数已保存到 infer.yaml")
                self._sync_ui_with_configs()
            except Exception as e:
                self.log_text.append(f"[错误] 保存推理参数失败: {e}")
        main_window.show()

    # -------- 文件操作槽函数 --------
    def open_image(self):
        file_path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.png *.jpg *.jpeg *.bmp)")
        if file_path:
            self.input_path_edit.setText(file_path)
            self._add_to_recent_files(file_path)

    def open_video(self):
        file, _ = QFileDialog.getOpenFileName(self, "选择视频", "", "Videos (*.mp4 *.avi *.mov)")
        if file:
            self.log_text.append(f"打开视频: {file}")
            self.input_path_edit.setText(file)
            self._add_to_recent_files(file)

    def open_camera(self):
        cam_id, ok = QInputDialog.getInt(self, "选择摄像头", "输入摄像头ID:", 0, 0, 10, 1)
        if ok:
            self.log_text.append(f"选择摄像头: {cam_id}")
            cam_str = f"{cam_id}"
            self.input_path_edit.setText(cam_str)
            self._add_to_recent_files(cam_str)

    def save_result(self):
        """保存检测结果"""
        if hasattr(self, 'last_detected_pixmap') and self.last_detected_pixmap and not self.last_detected_pixmap.isNull():
            default_name = "detection_result.png"
            if hasattr(self, '_last_input_source'):
                base_name = os.path.splitext(os.path.basename(str(self._last_input_source)))[0]
                default_name = f"{base_name}_detected.png"

            file, selected_filter = QFileDialog.getSaveFileName(
                self, "保存检测结果", default_name,
                "Images (*.png *.jpg *.jpeg);;All Files (*)"
            )
            if file:
                if self.last_detected_pixmap.save(file):
                    self.log_text.append(f"[保存] 检测结果已保存: {file}")
                else:
                    self.log_text.append(f"[错误] 无法保存检测结果: {file}")
        else:
            self.log_text.append("[提示] 暂无检测结果可保存")

    def batch_process(self):
        """批量处理文件夹中的图片和视频"""
        if not YOLO_AVAILABLE:
            self.log_text.append("[错误] 未安装 ultralytics，无法执行批量处理。请运行 'pip install ultralytics'。")
            return

        folder = QFileDialog.getExistingDirectory(self, "选择文件夹进行批量处理")
        if not folder:
            return

        self.log_text.append(f"[批量] 开始处理文件夹: {folder}")

        image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.tiff', '*.tif', '*.webp']
        video_extensions = ['*.mp4', '*.avi', '*.mov', '*.mkv', '*.wmv', '*.flv', '*.webm', '*.m4v']

        image_files = []
        for ext in image_extensions:
            image_files.extend(glob.glob(os.path.join(folder, ext)))
            image_files.extend(glob.glob(os.path.join(folder, ext.upper())))

        video_files = []
        for ext in video_extensions:
            video_files.extend(glob.glob(os.path.join(folder, ext)))
            video_files.extend(glob.glob(os.path.join(folder, ext.upper())))

        all_files = sorted(list(set(image_files + video_files)))
        if not all_files:
            self.log_text.append(f"[批量] 警告: 文件夹 {folder} 中未找到支持的图片或视频文件。")
            return

        total_files = len(all_files)
        self.log_text.append(f"[批量] 找到 {total_files} 个文件。")

        model_path = self.infer_params.get("model", "yolov8n.pt")
        device = self.infer_params.get("device", "cpu")

        model_needs_reload = (
            self._batch_model is None or
            self._batch_model_path != model_path
        )

        if model_needs_reload:
            try:
                self.log_text.append(f"[批量] 正在加载模型: {model_path} (设备: {device})")
                self._batch_model = YOLO(model_path)
                self._batch_model_path = model_path
                self.log_text.append(f"[批量] 模型加载成功。")
            except Exception as e:
                error_msg = f"[错误] 批量处理时加载模型失败: {e}"
                self.log_text.append(error_msg)
                self.log_text.append(traceback.format_exc())
                QMessageBox.critical(self, "模型加载错误", f"无法加载模型 {model_path}:\n{str(e)}")
                return

        try:
            conf_threshold = float(self.infer_params.get("conf", 0.25))
            iou_threshold = float(self.infer_params.get("iou", 0.45))
        except (ValueError, TypeError) as e:
            self.log_text.append(f"[错误] 从 infer.yaml 读取参数时出错 (conf/iou): {e}")
            conf_threshold = 0.25
            iou_threshold = 0.45

        show_boxes = self._get_bool_param(self.infer_params, "show_box", True)
        show_labels = self._get_bool_param(self.infer_params, "show_label", True)
        show_conf = self._get_bool_param(self.infer_params, "show_conf", True)
        save_images = True
        save_txt = self._get_bool_param(self.infer_params, "save_txt", False)

        project_dir = self.infer_params.get("project", "runs/detect")
        name = self.infer_params.get("name", "batch_predict")
        save_dir = os.path.join(project_dir, name)
        os.makedirs(save_dir, exist_ok=True)
        self.log_text.append(f"[批量] 结果将保存到: {save_dir}")

        processed_count = 0
        failed_count = 0
        for file_path in all_files:
            try:
                processed_count += 1
                self.log_text.append(f"[批量] ({processed_count}/{total_files}) 正在处理: {os.path.basename(file_path)}")

                results = self._batch_model.predict(
                    source=file_path,
                    conf=conf_threshold,
                    iou=iou_threshold,
                    device=device,
                    save=save_images,
                    save_txt=save_txt,
                    show_labels=show_labels,
                    show_conf=show_conf,
                    boxes=show_boxes,
                    project=project_dir,
                    name=name,
                    exist_ok=True,
                    verbose=False
                )
                self.log_text.append(f"[批量] ({processed_count}/{total_files}) 处理完成: {os.path.basename(file_path)}")

            except Exception as e:
                failed_count += 1
                error_msg = f"[错误] 处理文件 {file_path} 时失败: {e}"
                self.log_text.append(error_msg)
                self.log_text.append(traceback.format_exc())

        self.log_text.append(f"[批量] 处理完成。成功: {processed_count - failed_count}/{total_files}, 失败: {failed_count}。")
        self.log_text.append(f"[批量] 结果已保存至: {save_dir}")

    def close_app(self):
        self.log_text.append("退出应用")
        if hasattr(self, 'infer_thread') and self.infer_thread is not None and self.infer_thread.isRunning():
             self.infer_thread.stop()
             self.log_text.append("[清理] 推理线程已停止")
        if hasattr(self, 'train_thread') and self.train_thread is not None and self.train_thread.isRunning():
             self.train_thread.stop()
             self.log_text.append("[清理] 训练线程已停止")
        self.close()

    # ========== 新增功能实现 ==========

    # --- 最近文件功能 ---
    def _update_recent_files_menu(self):
        """更新最近文件菜单显示"""
        self.recent_files_menu.clear()
        settings = QSettings("YourCompany", "YOLODetector")
        recent_files = settings.value("recent_files", [], type=list)

        # 加载时去重
        unique_recent_files = []
        seen_paths = set()
        for path in recent_files:
            if path not in seen_paths:
                unique_recent_files.append(path)
                seen_paths.add(path)
        recent_files = unique_recent_files

        if not recent_files:
            no_files_action = QAction("无最近文件", self)
            no_files_action.setEnabled(False)
            self.recent_files_menu.addAction(no_files_action)
        else:
            for file_path in recent_files[:5]:
                action = QAction(file_path, self)
                action.triggered.connect(lambda checked, fp=file_path: self._load_recent_file(fp))
                self.recent_files_menu.addAction(action)

    def _add_to_recent_files(self, file_path):
        """将文件路径添加到最近文件列表"""
        if not file_path:
            return
        settings = QSettings("YourCompany", "YOLODetector")
        recent_files = settings.value("recent_files", [], type=list)

        if file_path in recent_files:
            recent_files.remove(file_path)
        recent_files.insert(0, file_path)
        recent_files = recent_files[:5]

        settings.setValue("recent_files", recent_files)
        self._update_recent_files_menu()

    def _load_recent_file(self, file_path):
        """加载最近打开的文件"""
        self.input_path_edit.setText(file_path)
        self.log_text.append(f"加载最近文件: {file_path}")

    # --- 同步UI控件与配置文件 ---
    def _sync_ui_with_configs(self):
        """根据 infer.yaml 和 train.yaml 同步 UI 控件状态"""
        # 同步 infer.yaml 参数到 UI
        if isinstance(self.infer_params, dict):
            # 模型路径已在 _sync_paths_to_ui 中处理或通过按钮设置
            # self.model_path_edit.setText(str(self.infer_params.get("model", ""))) 

            conf = self.infer_params.get("conf", 0.25)
            if isinstance(conf, (int, float)):
                self.slider_conf.setValue(int(conf * 100))

            iou = self.infer_params.get("iou", 0.45)
            if isinstance(iou, (int, float)):
                self.slider_iou.setValue(int(iou * 100))

            self.chk_show_box.setChecked(self._get_bool_param(self.infer_params, "show_box", True))
            self.chk_show_label.setChecked(self._get_bool_param(self.infer_params, "show_label", True))
            
        # 同步 train.yaml 参数到 UI (如果需要，例如 data 路径)
        # if isinstance(self.train_params, dict):
        #     data_path = self.train_params.get("data", "")
        #     if data_path:
        #         # 如果有显示 data 路径的 QLineEdit，可以在这里同步
        #         # self.data_config_path_edit.setText(data_path) 
        #         pass

        # 同步路径到 UI
        self._sync_paths_to_ui()

    def _sync_paths_to_ui(self):
        """将 infer.yaml 和 train.yaml 中的路径同步到 UI 控件"""
        # 同步模型路径 (来自 infer.yaml)
        if isinstance(self.infer_params, dict):
            model_path = self.infer_params.get("model", "")
            # 避免覆盖用户刚在 UI 上设置的路径（如果 UI 已经有内容且与 yaml 不同）
            # 一个更简单的策略是在初始化时同步，之后主要靠按钮操作更新 UI 和 yaml
            # 这里我们简单地同步，因为 _sync_ui_with_infer_params 也会设置
            # if not self.model_path_edit.text():
            self.model_path_edit.setText(str(model_path))

        # 同步数据集配置路径 (来自 train.yaml)
        # 注意：UI 中没有专门用于显示 data.yaml 路径的 QLineEdit (self.data_config_path_edit 不存在)
        if isinstance(self.train_params, dict):
            data_path = self.train_params.get("data", "")
            if data_path:
                # 如果没有 UI 控件显示，可以在日志中提示，或将来添加控件
                # self.log_text.append(f"[同步] 数据集配置: {data_path}")
                pass # 暂时无操作，因为我们没有对应的 UI 控件

    def _get_bool_param(self, params_dict, key, default):
        """从参数字典安全获取布尔值"""
        value = params_dict.get(key, default)
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            return value.lower() in ['true', '1', 'yes', 'on']
        if isinstance(value, (int, float)):
            return value != 0
        return bool(value)

    # --- UI控件信号处理函数 ---
    def _on_conf_slider_changed(self, value):
        conf_value = value / 100.0
        self.log_text.append(f"置信度阈值调整为: {conf_value:.2f}")
        self._update_infer_yaml("conf", conf_value)

    def _on_iou_slider_changed(self, value):
        iou_value = value / 100.0
        self.log_text.append(f"IOU阈值调整为: {iou_value:.2f}")
        self._update_infer_yaml("iou", iou_value)

    def _on_show_box_changed(self, state):
        is_checked = state == Qt.Checked
        self.log_text.append(f"显示目标框: {'是' if is_checked else '否'}")
        self._update_infer_yaml("show_box", is_checked)

    def _on_show_label_changed(self, state):
        is_checked = state == Qt.Checked
        self.log_text.append(f"显示类别标签: {'是' if is_checked else '否'}")
        self._update_infer_yaml("show_label", is_checked)

    # --- 目标过滤功能 (需要 dialogs/object_filter_dialog.py) ---
    # def _open_object_filter(self):
    #     default_classes = [
    #         'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
    #         # ... (COCO 类别列表)
    #     ]
    #     current_selected = []
    #     if isinstance(self.infer_params, dict):
    #         classes_param = self.infer_params.get("classes", None)
    #         if isinstance(classes_param, list):
    #             current_selected = [str(c) for c in classes_param if isinstance(c, str)]
    #         elif isinstance(classes_param, str):
    #             current_selected = [c.strip() for c in classes_param.split(',')]
    #     
    #     try:
    #         from dialogs.object_filter_dialog import ObjectFilterDialog
    #         dlg = ObjectFilterDialog(default_classes, current_selected, self)
    #         if dlg.exec_():
    #             selected_names = dlg.get_selected_classes()
    #             self.log_text.append(f"[过滤] 选择类别: {selected_names}")
    #             self._update_infer_yaml("classes", selected_names if selected_names else None)
    #     except ImportError:
    #         self.log_text.append("[错误] 未找到目标过滤对话框，请确保 dialogs/object_filter_dialog.py 已创建")
